应用 | Build with Mapbox —— 基于Mapbox GL JS 的 3D飞机大战游戏
最早接触 Mapbox 还是在读研的时候,当时觉得底图好漂亮,能把地图设计做到如此精益求精,算是数一数二的企业。后来接触 Mapbox GL JS, 体验到了畅快的三维地图开发,想想自己这么爱玩游戏,干嘛不开发一款 3D 多人飞机大战呢?
受群里研究 Mapbox 的热情和@扯淡大叔, @老羽,以及@F3earth 成员的帮助和启发, 突发奇想花了点时间基于 Mapbox GL JS 和 Socket.io 做了一个 3D 飞机大战游戏。
整体架构
简单来说,整个游戏的设计思路就是:
服务器:从启动开始就监听任何客户端发来了websocket 连接请求,有了连接(connection事件)后,就把客户端初次发来的用户名称、当前飞机的坐标、朝向作为一个client 加入客户端数据池(目前简单处理为clients数组)中。那么至此一个客户端的数据就同步到了服务器端了。
客户端:将地图中的飞机坐标、朝向等信息也定时发送(socket.send)给服务器端,以便于服务器端同时广播(broadcast)给所有其他客户端。其实服务端就是负责中转消息,目的是让所有客户端视野中的玩家的飞机状态保持一致。既然websocket是双向通信,客户端也需要定时发送消息给服务器端,并且更重要的是处理服务器端发来的各种消息(message事件),进而推动游戏的进度,以及分辨哪些是欢迎用户上线的消息,普通的玩家位置同步消息,抑或是 A 击败了 B 这样的消息。
Mapbox GL JS:借助 Mapbox GL JS 展现宏观的战场和各飞机的状态,以及用户可以通过键盘对自己的战机进行细微的操作。Mapbox GL JS 的 Webgl 渲染及完备的 API 给 3D游戏开发带来的很大的便利。^_^
后端及前端websocket
简单看看后端 Websocket 代码是如何实现的:
// 后端关键流程实现
var app = express(), server = require('http').createServer(app), io = require('socket.io').listen(server); // 引入socket.io 库
io.on('connection', function(socket) {
// 开始监听客户端的websocket连接请求,connection事件产生 socket 对象 socket.emit('open'); // 向该客户端发送open事件. // init client drone obj for each connection !! var client = { name: false, color: getColor(), direction: 0, coordinates: [0, 0] } // message from client. socket.on('message', function(msg) {
if (!client.name && msg.name) {
// 如果是第一次连接,把用户的名字存储起来,并且广播给所有客户端。
var obj = { }; // 构建发送给其他客户端的消息 obj = msg;
clients.push(client); // 加入后台维持的客户端数组
socket.broadcast.emit('message', obj); // 广播欢迎语给其他客户端 } else if ( client.name == msg.name ) { // 客户端发来的飞机状态消息 // 广播给其他客户端
socket.broadcast.emit('message', obj); } } }
后台处理过程相对简单,基本只需接受某客户端发来的消息,转发给其他客户端即可(随机敌机位置什么的就不讲了,当然后期要改成所有客户端共享一套敌机信息,这样就可以一起打同一个BOSS了)。
前端业务相对复杂, 除了应对websocket 消息之外,需要构建一套飞机的数据模型,包括位置,速度,朝向,血量,武器装备等(可以非常复杂,目前就简单处理)。
var socket;
try { socket = io.connect("http://123.206.201.245:3002");
socket.on('open', function(){ // 当服务端确认连接后,开始发送第一次数据。 statusBar.innerText = "已经连上服务器..";
var askName = prompt("来,取个名字", ""); }
socket.on("message", function(json) {
// 其实收到的是js 对象,这一点很牛逼。因为双向通信过程中传递的是 Binary 形式的数据,不需要再次解析了。 if (json.type === "welcome" && json.text.name) {
// .. 显示其他用户登录消息 }
else if (json.type === "defeat") {
// .. 在前端的敌机数据模型中移除空血槽的飞机 }
else if (drone && json.text.name != drone.name) {
// .. 传来的其他客户端飞机消息
featureCol.features.forEach(function(drone) {
// featureCol 是所有敌机数据集合,根据用户名check是更新还是新增. } } }
}
Mapbox GL JS 处理地图中的飞机、子弹及敌机
飞机的状态数据需要定时上传服务器,同时借助 VectorLayer 渲染在 Map 中。渲染过程采用 GeoJSON 对象 作为飞机矢量图层 的数据源(source)。那么是否是一接到服务器端消息就去重绘所有飞机位置呢? 并不是这样,为了性能不那么差,这边通过全局的一个定时器去统一调用source 的 setData() 方法,实现飞机最新的状态重绘。
map.getSource('drone').setData(featureCol);
飞机子弹的轨迹计算,涉及到用户按下空格键的瞬间飞机的位置和朝向,根据设定的子弹飞行时间做一个动画显示。
var bulletSource;
var bulletTimer = setInterval(renderBullet, 30);
// common function for render myDrone and other client's fire
function renderBullet() {
var steplength = 0.02;
particles.coordinates = [];
// if drone is firing, it's bullet coordiantes be calculated and rendered. for (var j = 0; j < drones.length; j++) {
var hitted = false;
if (drones[j].firing && drones[j].bullet) { drones[j].bullet.spoint.coordinates[0] += Math.sin(drones[j].bullet.direction)*steplength; drones[j].bullet.spoint.coordinates[1] += Math.cos(drones[j].bullet.direction)*steplength;
var real_point = drones[j].bullet.spoint;
particles.coordinates.push(real_point.coordinates);
for (var i = 0; i < 9; i++) {
var particle = [];
// 0.01 is step length of bullet each frames.. particle.push(real_point.coordinates[0] - Math.sin(drones[j].bullet.direction)*steplength*i/zoom);
particle.push(real_point.coordinates[1] - Math.cos(drones[j].bullet.direction)*steplength*i/zoom);
particles.coordinates.push(particle); } } } bulletSource = map.getSource('drone-target');
if (bulletSource) {
bulletSource.setData(particles); } }
子弹和敌机的碰撞检测,简化处理:设定一个常数作为飞机体积,在子弹飞行过程中实时计算子弹和敌机实际地理距离,小于飞机体积,则判定为碰撞。
function testCrash(coordinates, name) {
var distance, volume = 0.20/zoom,
featureIndex = 0,
damagedIndex = -1,
damagedDroneName = 0,
hitted = false; ...... // 只用来做本玩家对其他飞机的射击检测!!! if (damagedDroneName && damagedIndex > -1 ){
var damagedFeature = findInFeatures(damagedDroneName);
var damagedDrone = findInDrones(damagedDroneName);
if (!damagedDrone) return;
if (damagedDrone.life)
damagedDrone.life -= 1;
if (name === drone.name && !damagedDrone.life) { totalKill += 1;
statsBar.innerText = "Kill " + totalKill; }
if (!damagedDrone.life) {
// explode effect on damagedFeature. explode(damagedFeature, firingTime - 200);
if (damagedDroneName !== drone.name) {
setTimeout(function(){
delInDrones(damagedDroneName); }, 50); setTimeout(function(){
delInFeatureCol(damagedDroneName); }, firingTime - 100); } } hitted = true; }
return hitted; }
敌机自动跟随最近玩家的设定。这个奇怪的设定是后来加上去的,因为最开始的敌机我设定为攻击最近的飞机,不论是Robot还是玩家,并且敌机移动是随机方向的。后来为了玩家有种被追杀、热血沸腾的感觉,设定为了追击玩家。。
另外挑两点比较有意思的Code分享下,第一点是 Robot 敌机的随机行为控制:
// setPostion is to update Mydrone position.function setPosition() {
// direction in Rad. Generally, 1 Rad stands for 100km var current_rotate = map.getBearing(); if (!manual && Math.random() > 0.95) {
// 这边有意思,在每秒50帧的情况下,不是每一帧都会随机微调飞机的方向。而是5%的概率。 direction += (Math.random() - 0.5) /5; } // 根据飞机朝向和速度更新位置。 point.coordinates[0] += speed * Math.sin(direction) / 100;
point.coordinates[1] += speed * Math.cos(direction) / 100;
// 校正飞机的朝向显示。因为默认情况下mapbox是根据你的视角随时调整图标方向。但实际上飞机图标的朝向必须和飞机运行方向一致,而不是简单的和标注一样。 current_rotate = (-current_rotate) + direction * (180 / Math.PI); }
第二点是子弹飞行的计算过程:
// start: fire location, target: bullet destination, duration: total animation timefunction renderBulvar(start, target, direction, duration) {
// target is geojson POINT, add Temp point in layer.. var interval = 20,
ratio = interval/duration,
real_point = start,
range = 0.4,
count = 0,
hitted = false;
if (target.coordinates) {
var targetSource = map.getSource('drone-target');
window.setInterval(function(){
if (count > duration/interval) {
// 到达终点,不计算了 } else {
// 子弹每一帧跑一定比例的路程,最终到达指定终点 real_point.coordinates[0] += Math.sin(direction)*ratio*range;
real_point.coordinates[1] += Math.cos(direction)*ratio*range;
targetSource.setData(real_point);
if (!hitted){ hitted = testCrash(real_point.coordinates); // 感觉这里的hitted 有问题. } count += 1; } }, interval); } }
写在最后
为了游戏完整性,陆续加上了聊天系统、 飞机生命值、小地图以及敌机状态面板(鼠标悬停任意战机,查看当前状态)。
到这里其实基本介绍了这个游戏的制作过程,经历了一些不成熟的想法,总共花了十几个小时完成目前的开发。还没有严谨地考虑过代码结构和重构,有好些 Bug 都在慢慢去修改。感兴趣的童鞋,想在线体验游戏或者有任何建议的,欢迎访问github项目地址: Jqmap2 。
本文由 Alex 同学撰写,非常感谢Alex 同学的分享。还不赶快参加 Mapbox 组织的 "Build with Mapbox" 活动,直接联系我们分享使用 Mapbox 产品的开发经验,赢取礼物🎁和奖金!